//-----------------------------------------------------------------------
// <copyright file="MsTest.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <author>Andy Reeves</author>
// <email></email>
// <date>2005-06-02</date>
// <summary>Executes all testcases in an assembly using MsTest.</summary>
//-----------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks.Tools
{
    using System;
    using System.Xml;
    using System.Globalization;
    using System.IO;
    using System.Text;
    using System.Collections;
    using System.Reflection;
    using Microsoft.Build.Framework;
    using Microsoft.Build.Tasks;
    using Microsoft.Build.Utilities;

    public class MsTest : ToolTaskBase
    {


        private string[] testLists;
        private string[] tests;
        private string[] testContainers;
        private int testPass;
        private int testFail;
        private int testNotRun;
        private int testErrors;
        private double testTime;
        private int testTotal;
        private string[] failFullTypeNames;
        private string[] failMethodNames;

        /// <summary>
        /// Initializes a new instance of the MsTest class.
        /// </summary>
        public MsTest()
        {
        }

        /*
        public string[] Assemblies
        {
            get { return (string[])Bag["Assemblies"]; }
            set { Bag["Assemblies"] = value; }
        }
        */

        /// <summary>
        /// Path and file to save the test results
        /// </summary>
        /// <value>Fully qualified path name of the folder to output files to</value>
        [Required]
        public string OutputFolder
        {
            get
            {
                object temp = Bag["OutputFolder"];
                return (temp == null) ? String.Empty : (string)temp;
            }

            set { Bag["OutputFolder"] = value; }
        }

        /// <summary>
        /// The path to the xsl file to transform the final results
        /// </summary>
        /// <value></value>
        public string OutputXmlTransformFile
        {
            get
            {
                object temp = Bag["OutputXmlTransformFile"];
                return (temp == null) ? String.Empty : (string)temp;
            }
            set { Bag["OutputXmlTransformFile"] = value; }
        }

        /// <summary>
        /// Version of MsTest used for testing
        /// </summary>
        /// <value>8.0</value>
        [Required]
        public string Version
        {
            get { return (string)Bag["Version"]; }
            set { Bag["Version"] = value; }
        }

        /// <summary>
        /// The test meta data file to load
        /// </summary>
        /// <value></value>
        public string MetaDataFile
        {
            get
            {
                object temp = Bag["MetaDataFile"];
                return (temp == null) ? String.Empty : (string)temp;
            }
            set { Bag["MetaDataFile"] = value; }
        }

        /// <summary>
        /// The working directory to find the test assemblies in
        /// </summary>
        /// <value></value>
        public string WorkingDirectory
        {
            get
            {
                object temp = Bag["WorkingDirectory"];
                return (temp == null) ? String.Empty : (string)temp;
            }
            set { Bag["WorkingDirectory"] = value; }
        }

        /// <summary>
        /// The Run Config File to use
        /// </summary>
        /// <value></value>
        //[Required] no longer "required" jimhartill 11/07
        public string RunConfigFile
        {
            get
            {
                object temp = Bag["RunConfigFile"];
                return (temp == null) ? String.Empty : (string)temp;
            }
            set { Bag["RunConfigFile"] = value; }
        }

        /// <summary>
        /// The test lists to run
        /// </summary>
        /// <value>TestList names i.e. Rig/Smoke;SingleBox/Functional</value>
        //[Required] no longer "required" jimhartill 11/07
        public string IncludeTestList
        {
            get
            {
                object temp = Bag["IncludeTestList"];
                return (temp == null) ? String.Empty : (string)temp;
            }
            set { Bag["IncludeTestList"] = value; }
        }

        /// <summary>
        /// Tests
        /// </summary>
        /// <value>List of tests to execute</value>
        public string IncludeTests
        {
            get
            {
                object temp = Bag["IncludeTests"];
                return (temp == null) ? String.Empty : (string)temp;
            }
            set { Bag["IncludeTests"] = value; }
        }

        /// <summary>
        /// Test Containers
        /// </summary>
        /// <value>List of test containers</value>
        public string IncludeTestContainers
        {
            get
            {
                object temp = Bag["IncludeTestContainers"];
                return (temp == null) ? String.Empty : (string)temp;
            }
            set { Bag["IncludeTestContainers"] = value; }
        }

        /// <summary>
        /// Test Lists executed
        /// </summary>
        /// <value>The test lists executed</value>
        [Output]
        public string[] TestLists
        {
            get { return this.testLists; }
        }

        /// <summary>
        /// Tests executed
        /// </summary>
        /// <value>List of tests executed</value>
        [Output]
        public string[] Tests
        {
            get { return this.tests; }
        }

        /// <summary>
        /// Test Containers
        /// </summary>
        /// <value>List of test containers</value>
        [Output]
        public string[] TestContainers
        {
            get { return this.testContainers; }
        }

        /// <summary>
        /// Test passed
        /// </summary>
        /// <value>A list of tests passed</value>
        [Output]
        public int Pass
        {
            get { return this.testPass; }
        }

        /// <summary>
        /// Tests failed
        /// </summary>
        /// <value>A list of tests failed</value>
        [Output]
        public int Fail
        {
            get { return this.testFail; }
        }

        /// <summary>
        /// Tests that have errored
        /// </summary>
        /// <value>number of tests that have errors</value>
        [Output]
        public int Errors
        {
            get { return this.testErrors; }
        }
        /// <summary>
        /// TestNames that failed
        /// </summary>
        /// <value>The test that failed for each testlist</value>
        [Output]
        public string[] FailFullTypeNames
        {
            get { return this.failFullTypeNames; }
        }

        /// <summary>
        /// Test method names that failed
        /// </summary>
        /// <value>The test that failed for each testlist</value>
        [Output]
        public string[] FailMethodNames
        {
            get { return this.failMethodNames; }
        }

        /// <summary>
        /// Tests not ran
        /// </summary>
        /// <value>A list of tests not ran</value>
        [Output]
        public int NotRun
        {
            get { return this.testNotRun; }
        }

        /// <summary>
        /// A list of all tests within the specified assembly
        /// </summary>
        /// <value>A complete list of tests</value>
        [Output]
        public int Total
        {
            get { return this.testTotal; }
        }

        /// <summary>
        /// Time taken to test the specified assembly
        /// </summary>
        /// <value>Run time</value>
        [Output]
        public double TimeTaken
        {
            get { return this.testTime; }
        }

        /// <summary>
        /// The name of the exe we call.
        /// </summary>
        /// <value>The name of the exe we call.</value>
        protected override string ToolName
        {
            get { return "mstest.exe"; }
        }

        /// <summary>
        /// Performs the action of this task.
        /// </summary>
        protected override void InternalExecute()
        {
            string outputXmlFilename = String.Empty;
            string testListWithoutSlashes = String.Empty;

            if (!String.IsNullOrEmpty(IncludeTestList))
            {
                this.testLists = IncludeTestList.Split(';');
                //keep them sorted for a BinarySearch later
                Array.Sort(this.testLists);
                for (int i = 0; i < this.TestLists.Length; i++)
                {
                    testListWithoutSlashes += this.testLists[i].Replace('/', '.');
                }
            }

            if (!String.IsNullOrEmpty(IncludeTests))
            {
                this.tests = IncludeTests.Split(';');
            }

            if (!String.IsNullOrEmpty(IncludeTestContainers))
            {
                this.testContainers = IncludeTestContainers.Split(';');
            }

            CommandLineBuilder commandLine = new CommandLineBuilder();

            Directory.CreateDirectory(this.OutputFolder);

            if (this.testContainers.Length != 0)
            {
                FileInfo fi = new FileInfo(this.testContainers[0]);
                outputXmlFilename = Path.Combine(this.OutputFolder, fi.Name + ".TestResult.trx");
            }
            else
            {
                if (!String.IsNullOrEmpty(testListWithoutSlashes))
                {
                    outputXmlFilename = Path.Combine(this.OutputFolder, testListWithoutSlashes + ".TestResult.trx");
                }
            }


            this.GenerateCommandLineCommands(commandLine, outputXmlFilename);
            Log.LogMessage(MessageImportance.Low, "Args are" + commandLine.ToString());
            string currentWorkingDirectory = Environment.CurrentDirectory;

            if (this.WorkingDirectory != String.Empty)
            {
                Environment.CurrentDirectory = this.WorkingDirectory;
            }
            base.ExecuteTool(this.ComputePathToTool(), String.Empty, commandLine.ToString());
            if (this.WorkingDirectory != String.Empty)
            {
                Environment.CurrentDirectory = currentWorkingDirectory;
            }
            if (!System.IO.File.Exists(outputXmlFilename))
            {
                //We have no output file something bad happened in mstest.
                Log.LogMessageFromResources("MsTest.FatalError");
                this.testFail = Int32.MaxValue;
                return;
            }
            this.ProcessXmlResultsFile(outputXmlFilename);
        }

        /// <summary>
        /// Full calculated path to tool including exe name
        /// </summary>
        /// <returns>Defaults to default install location of mstest</returns>
        protected override string GenerateFullPathToTool()
        {
            // C:\Program Files\Microsoft Visual Studio 8\Common7\IDE
            string mstestPath = string.Empty;
            mstestPath = Path.Combine(this.GetPathToBinFolder(), this.ToolName);
            return mstestPath;
        }

        private bool GenerateCommandLineCommands(CommandLineBuilder commandLine, string outputXmlFilename)
        {
            // switch on the version number and build appropriate command line
            string mstestVersion = (string)Bag["Version"];

            switch (mstestVersion)
            {
                case "8.0":
                case "9.0":
                    {
                        AppendSwitchAndValueWhenNotStringEmpty(commandLine, "/testmetadata", "MetaDataFile", ":");
                        AppendSwitchAndValueWhenNotStringEmpty(commandLine, "/runconfig", "RunConfigFile", ":");
                        commandLine.AppendSwitch("/resultsfile:" + outputXmlFilename);

                        if (this.testLists != null)
                        {

                            for (int i = 0; i < this.testLists.Length; i++)
                            {
                                commandLine.AppendSwitch("/testlist:" + this.testLists[i]);
                            }
                        }
                        if (this.tests != null)
                        {
                            for (int i = 0; i < this.tests.Length; i++)
                            {
                                //make sure the path is quoted correctly
                                string path = AddQuotesToPath(this.tests[i]);
                                commandLine.AppendSwitch("/test:" + path);
                            }
                        }
                        if (this.testContainers != null)
                        {
                            for (int i = 0; i < this.testContainers.Length; i++)
                            {
                                //make sure the path is quoted correctly
                                string path = AddQuotesToPath(this.testContainers[i]);
                                commandLine.AppendSwitch("/testcontainer:" + path);
                            }
                        }
                        break;
                    }
            }
            return true;
        }

        /// <summary>
        /// Adds the correct quotes to our paths to make sure mstest picks them up ok.
        /// </summary>
        /// <param name="pathToQuote">String to quote</param>
        /// <returns>Quoted string</returns>
        private string AddQuotesToPath(string pathToQuote)
        {
            pathToQuote = pathToQuote.Insert(0, "\"");
            pathToQuote = pathToQuote.Insert(pathToQuote.Length, "\"");
            return pathToQuote;
        }

        /// <summary>
        /// Processes the mstest results
        /// </summary>
        private void ProcessXmlResultsFile(string outputXmlFilename)
        {
            XmlDocument doc = new XmlDocument();

            try
            {
                doc.Load(outputXmlFilename);
            }
            catch (Exception e)
            {
                Log.LogErrorFromException(e);
                return;
            }

            int total = 0;
            int passed = 0;
            int executed = 0;
            int failed = 0;
            int errors = 0;
            
            if (this.Version == "9.0")
            {
                XmlNamespaceManager ns = new XmlNamespaceManager(doc.NameTable);
                ns.AddNamespace("prefix", "http://microsoft.com/schemas/VisualStudio/TeamTest/2006");
                XmlNode node = doc.SelectSingleNode("/prefix:TestRun/prefix:ResultSummary/prefix:Counters", ns);

                total = Convert.ToInt32(node.Attributes["total"].Value);
                passed = Convert.ToInt32(node.Attributes["passed"].Value);
                executed = Convert.ToInt32(node.Attributes["executed"].Value);
                failed = Convert.ToInt32(node.Attributes["failed"].Value);
                errors = Convert.ToInt32(node.Attributes["error"].Value);
                this.testTotal = total;
                this.testNotRun = total - executed;
                this.testPass = passed;
                this.testFail = failed;
                this.testErrors = errors;
                
            }
            else
            {

                XmlNode totalTestCount = doc.SelectSingleNode(@"//Tests/TestRun/result/totalTestCount");
                XmlNode passedTestCount = doc.SelectSingleNode(@"//Tests/TestRun/result/passedTestCount");
                XmlNode executedTestCount = doc.SelectSingleNode(@"//Tests/TestRun/result/executedTestCount");
                total = Convert.ToInt32(totalTestCount.InnerText);
                passed = Convert.ToInt32(passedTestCount.InnerText);
                executed = Convert.ToInt32(executedTestCount.InnerText);
                this.testTotal = total;
                this.testNotRun = total - executed;
                this.testPass = passed;
                this.testFail = executed - passed;

                XmlNodeList durationNodes = doc.SelectNodes(@"//Tests/UnitTestResult/duration");

                double timeTaken = 0;

                foreach (XmlNode node in durationNodes)
                {
                    timeTaken = TimeSpan.Parse(node.InnerText).TotalSeconds;
                    this.testTime += timeTaken;
                }

                XmlNodeList unitTestResultNodes = doc.SelectNodes(@"//Tests/UnitTestResult[outcome/value__='3']");
                if (unitTestResultNodes == null)
                {
                    return;
                }

                ArrayList fullTypeNameList = new ArrayList();
                ArrayList methodNameList = new ArrayList();

                foreach (XmlNode unitTestResultNode in unitTestResultNodes)
                {
                    string id = unitTestResultNode.SelectSingleNode("id/testId/id").InnerText;

                    XmlNode testValueNode = doc.SelectSingleNode("//Tests/TestRun/tests/value[id/id='" + id + "']");

                    string className = testValueNode.SelectSingleNode("testMethod/className").InnerText;
                    string methodName = testValueNode.SelectSingleNode("testMethod/name").InnerText;

                    fullTypeNameList.Add(methodName + ", " + className);
                    methodNameList.Add(methodName);

                }
                this.failFullTypeNames = new string[fullTypeNameList.Count];
                fullTypeNameList.CopyTo(this.failFullTypeNames);

                this.failMethodNames = new string[methodNameList.Count];
                methodNameList.CopyTo(this.failMethodNames);
            }
            if (executed != passed || errors != 0)
            {
                Log.LogMessageFromResources("MsTest.UnitTestFailed", outputXmlFilename);
            }
        }

        /// <summary>
        /// Find the correct path containing the version of mstest specified.
        /// </summary>
        /// <returns>mstest bin directory</returns>
        private string GetPathToBinFolder()
        {
            string mstestPath = string.Empty;

            switch (this.Version)
            {
                case "8.0":
                    {
                        mstestPath = System.Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\Microsoft Visual Studio 8\Common7\IDE");
                        break;
                    }
                case "9.0":
                    {
                        mstestPath = System.Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\Microsoft Visual Studio 9.0\Common7\IDE");
                        break;
                    }
                default:
                    {
                        mstestPath = System.Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\Microsoft Visual Studio 8\Common7\IDE");
                        break;
                    }
            }
            return mstestPath;
        }
    }
}
